Skip to content

Add support for mypy - #31

Draft
moi15moi wants to merge 3 commits into
mainfrom
Support-mypy
Draft

Add support for mypy#31
moi15moi wants to merge 3 commits into
mainfrom
Support-mypy

Conversation

@moi15moi

Copy link
Copy Markdown
Owner

@junkmd Do you have any idea how I can remove all the typing errors caused by comtypes?

Currently, I simply ignore all errors that look like this:

find_system_fonts_filename\windows\windows_fonts.py:51: error: "_Pointer[IDWriteFontFile]" has no attribute "GetLoader"  [attr-defined]

However, I was wondering if there’s a way to properly fix these errors. Even if I replace POINTER[...] with POINTER(...), I still get other errors, such as:

find_system_fonts_filename\windows\windows_fonts.py:316: error: Invalid type comment or annotation  [valid-type]
find_system_fonts_filename\windows\windows_fonts.py:316: note: Suggestion: use POINTER[...] instead of POINTER(...)

Would appreciate any insights!

@junkmd

junkmd commented Feb 24, 2025

Copy link
Copy Markdown

Hi, @moi15moi

The primary reason these type checker errors occur is that there is a discrepancy between the members dynamically defined by metaprogramming using ctypes and the members accessible through static analysis of the codebase performed by the type checker.
(Conversely, within the comtypes project itself, type checker errors do not occur in areas where these measures have been implemented.)

You can potentially resolve these errors by addressing the following points:

  1. Methods defined in _methods_ or _disp_methods_ are not recognized by the type checker. Therefore, you need to define type-annotated methods and attributes within an if TYPE_CHECKING block to enable the type checker to recognize these members.

  2. When type-annotating a pointer, you must use the _Pointer class, such as "_Pointer[Something]". Unlike PEP 585 collections, the _Pointer class cannot be used as a runtime generic (because __class_getitem__ is not implemented), so type annotations must be done with string literals.

  3. An instance obj created by CreateObject (and similar functions) satisfies both isinstance(obj, IUnknown) and isinstance(obj, POINTER(IUnknown)) at runtime, but this cannot be determined by the type checker. Similarly, while a pointer to IUnknown can access QueryInterface at runtime, _Pointer[IUnknown] is not treated as having QueryInterface in static analysis. In comtypes, by type-annotating to return IUnknown even when _Pointer[IUnknown] is returned, the type checker and LSP can reference QueryInterface. This correspondence is also described in the QueryInterface comments.

  4. You can avoid type checker errors by defining high-level wrapper methods in the interface and having them return pointers to the interface directly through method calls, rather than through pointer pass-by-reference.

If I were to extract a portion of this project's codebase and rewrite it to align more closely with the typed Python style, it might look like the following. I hope this serves as a helpful reference.

 class IDWriteFactory(IUnknown):
     # https://learn.microsoft.com/en-us/windows/win32/api/dwrite/nn-dwrite-idwritefactory
     _iid_ = GUID("{b859ee5a-d838-4b5b-a2e8-1adc7d93db48}")
     _methods_ = [
         ...
         STDMETHOD(HRESULT, "RegisterFontFileLoader", [POINTER(IDWriteFontFileLoader)]),
         ...
         STDMETHOD(HRESULT, "GetGdiInterop", [POINTER(POINTER(IDWriteGdiInterop))]),
         ...
     ]

+    if TYPE_CHECKING:
+        def RegisterFontFileLoader(self, fontfileloader: IDWriteFontFileLoader) -> int: ...
+
+    def GetGdiInterop(self) -> IDWriteGdiInterop:
+        ptr = POINTER(IDWriteGdiInterop)()
+        self.__com_GetGdiInterop(byref(ptr))
+        return ptr  # type: ignore


+_T_IUnknown = TypeVar("_T_IUnknown", bound=IUnknown)


 class DWrite:
     def __init__(self) -> None:
         dwrite = windll.LoadLibrary("dwrite")

         # https://learn.microsoft.com/en-us/windows/win32/api/dwrite/nf-dwrite-dwritecreatefactory
-        self.DWriteCreateFactory = dwrite.DWriteCreateFactory
-        self.DWriteCreateFactory.restype = HRESULT
-        self.DWriteCreateFactory.argtypes = [wintypes.UINT, POINTER(IID), POINTER(POINTER(IUnknown))]
+        self._DWriteCreateFactory = dwrite.DWriteCreateFactory
+        self._DWriteCreateFactory.restype = HRESULT
+        self._DWriteCreateFactory.argtypes = [wintypes.UINT, POINTER(IID), POINTER(POINTER(IUnknown))]
+
+    def DWriteCreateFactory(self, factorytype: int, interface: Type[_T_IUnknown], /) -> _T_IUnknown:
+        ptr = POINTER(interface)()
+        self._DWriteCreateFactory(factorytype, byref(interface._iid_), byref(ptr))
+        return ptr  # type: ignore


 dwrite = DWrite()
-dwrite_factory = POINTER(IDWriteFactory)()
-dwrite.DWriteCreateFactory(DWRITE_FACTORY_TYPE.DWRITE_FACTORY_TYPE_ISOLATED, byref(dwrite_factory._iid_), byref(dwrite_factory))
-gdi_interop = POINTER(IDWriteGdiInterop)()
-dwrite_factory.GetGdiInterop(byref(gdi_interop))
+dwrite_factory = dwrite.DWriteCreateFactory(DWRITE_FACTORY_TYPE.DWRITE_FACTORY_TYPE_ISOLATED, IDWriteFactory)
+gdi_interop = dwrite_factory.GetGdiInterop()

@moi15moi

Copy link
Copy Markdown
Owner Author

Thank you very much for your detailed answer!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants